package org.cmroot.certmaster.service;

import org.cmroot.certmaster.dao.PKCS10Dao;
import org.cmroot.certmaster.dao.X509Dao;
import org.cmroot.certmaster.model.ForDTO;
import org.cmroot.certmaster.model.PKCS10;
import org.cmroot.certmaster.model.X509;
import org.cmroot.certmaster.shared.PKIServiceException;
import org.cmroot.certmaster.dao.GenericDaoException;
import org.cmroot.certmaster.shared.dto.DTO;
import org.cmroot.certmaster.shared.dto.Pkcs10DTO;
import org.cmroot.certmaster.shared.dto.X509DTO;
import org.cmroot.certmaster.shared.dto.ext.ExtensionDTO;
import org.cmroot.certmaster.shared.dto.ext.Pkcs10ExtDTO;
import org.cmroot.certmaster.shared.dto.ext.X509ExtDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.*;

/**
 * Copyright (c) 2010 Andrey Domas <andrey.domas at gmail.com>.
 * *
 * This file is part of certmaster.
 * *
 * Certmaster is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * *
 * Certmaster is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see http://www.gnu.org/licenses
 * or write to the Free Software Foundation,Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301  USA
 */

@Service
@Qualifier("pkiservice")
public class PkiService {

    @Autowired
    private LogBuilder logBuilder;

    @Autowired
    @Qualifier("pkcs10_hibernate")
    private PKCS10Dao requestDao;

    @Autowired
    @Qualifier("x509_hibernate")
    private X509Dao certDao;

    @Autowired
    @Qualifier("bc_engine")
    private PKIEngine pkiEngine;

    @Autowired
    @Qualifier("asn1_bc_engine")
    private ASN1Engine asn1Engine;

    /**
     * Load new certificate request from PEM
     *
     * @param text certificate request, PEM
     * @throws PKIServiceException wrong or broken PEM
     */
    public void newRequest(String text) throws PKIServiceException {

        byte der_csr[];

        //check PEM and create request object
        try {
            der_csr = pkiEngine.readRequest(text);
        } catch (PKIEngineException e) {
            throw new PKIServiceException(e);
        }

        PKCS10 req = new PKCS10(der_csr);
        req.setCN( (String)pkiEngine.readRequest(der_csr).get(0) );

        //save object if it success. created
        try {
            requestDao.save(req);
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }
    }

    /**
     * Create DTO from ForDTO objects collection. Null safe.
     *
     * @param what
     * @param where memory must be allocated
     * @return 'where' argument
     */
    private <FOR_DTO extends ForDTO, RES_DTO extends DTO,
            COL_FOR_DTO extends Collection<FOR_DTO>, COL_RES_DTO extends Collection<RES_DTO>>
    COL_RES_DTO prepareDTO(COL_FOR_DTO what, COL_RES_DTO where) {

        //check for null or empty daos
        if (what == null || what.size() < 1) {
            return where;
        }

        for (FOR_DTO w : what) {
            where.add((RES_DTO) w.createDTO());
        }
        return where;
    }

    /**
     * Get all certificates and requests
     *
     * @return List of certificates and requests
     * @throws PKIServiceException
     */
    public List<Pkcs10DTO> getAllPKCS10() throws PKIServiceException {
        List<PKCS10> pkcs10_dao;
        try {
            pkcs10_dao = requestDao.findAll();
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }

        //create DTO
        List<Pkcs10DTO> dto_list = new ArrayList<Pkcs10DTO>();
        return prepareDTO(pkcs10_dao, dto_list);
    }

    /**
     * Get all PKI tree (top level and children)
     *
     * @return
     * @throws PKIServiceException
     */
    public List<X509DTO> getAllX509() throws PKIServiceException {
        List<X509> x509_dao;
        try {
            x509_dao = certDao.findAll();
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }

        //create DTO
        List<X509DTO> dto_list = new ArrayList<X509DTO>();
        return prepareDTO(x509_dao, dto_list);
    }

    public List<X509DTO> getCA() throws PKIServiceException {
        List<X509> x509_dao;
        try {
            x509_dao = certDao.findCA();
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }

        //create DTO
        List<X509DTO> dto_list = new ArrayList<X509DTO>();
        return prepareDTO(x509_dao, dto_list);
    }

    /**
     * Get all children of specified X509DTO parent
     *
     * @param parent_id - parent X509 ID, if null - only selfsigned certs
     * @return
     * @throws PKIServiceException
     */
    public List<X509DTO> getX509Children(Long parent_id) throws PKIServiceException {
        List<X509> x509_dao;
        try {
            x509_dao = certDao.findByIssuer((parent_id == null) ? null : certDao.findById(parent_id));
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }

        //create DTO
        List<X509DTO> dto_list = new ArrayList<X509DTO>();
        return prepareDTO(x509_dao, dto_list);
    }

    /**
     * Get X509 by specified ID
     *
     * @param id
     * @return
     * @throws PKIServiceException
     */
    public X509DTO getX509(Long id) throws PKIServiceException {
        try {
            return certDao.findById(id).createDTO();
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }
    }

    /**
     * Remove object from datastore by id and DTO class name
     *
     * @param clazz - DTO class name
     * @param objId - id for delete
     * @throws PKIServiceException
     */
    public void removePKCS(String clazz, Long objId) throws PKIServiceException {

        Class c;
        try {
            c = Class.forName(clazz);
        } catch (ClassNotFoundException e) {
            throw new PKIServiceException(e);
        }

        try{
            if (c.equals(Pkcs10DTO.class)) {
                logBuilder.append("Remove certificate request with id ").append(objId);
                requestDao.delete(objId);
            } else if (c.equals(X509DTO.class)) {
                logBuilder.append("Remove certificate with id ").append(objId);
                certDao.delete(objId);
            }
        } catch (GenericDaoException e) {
            logBuilder.append(": error");
            logBuilder.writeInfo();
            throw new PKIServiceException(e);
        } finally {
            logBuilder.writeInfo();
        }

    }

    private static X509 certObj2X509(X509Certificate o) throws PKIServiceException {
        X509 x509 = new X509();
        x509.setCN(o.getSubjectDN().toString().replaceAll("(.*CN=)|(,.*)", ""));
        x509.setNotAfter(o.getNotAfter());
        x509.setNotBefore(o.getNotBefore());
        x509.setSerial(o.getSerialNumber());
        x509.setCA( (o.getBasicConstraints() != -1) );

        try {
            x509.setDer(o.getEncoded());
        } catch (CertificateEncodingException e) {
            throw new PKIServiceException(e);
        }
        return x509;
    }

    /**
     * Import certificate chain
     *
     * @param text certificate chain, PEM
     * @return certificate chain for user verify
     * @throws PKIServiceException
     */
    public List<X509DTO> importChain(String text) throws PKIServiceException {

        Collection<X509Certificate> certsObj_col;
        try {
            certsObj_col = pkiEngine.readCert(text);
        } catch (PKIEngineException e) {
            throw new PKIServiceException(e);
        }


        HashMap<String, X509> result_map = new HashMap<String, X509>();

        for (X509Certificate certObj : certsObj_col) {
            boolean object_already_exist = false;

            X509 cert = certObj2X509(certObj);
            //check if cert already exist
            if (result_map.containsKey(new String(cert.getDer()))) {
                cert = result_map.get(new String(cert.getDer()));
                object_already_exist = true;
            }

            //try to link issuers and its certs
            for (X509Certificate issuerObj : certsObj_col) {
                if (certObj.getIssuerDN().equals(issuerObj.getSubjectDN())) {

                    boolean my_issuer = true;

                    //verify - my issuer or not?
                    try {
                        pkiEngine.verifyChain(issuerObj, null, certObj);
                    } catch (GeneralSecurityException e) {
                        my_issuer = false;
                    }

                    if (my_issuer) {
                        //self-signed?
                        if (certObj.equals(issuerObj)) {
                            cert.setSelfSigned(true);
                        } else {
                            //no, attach issuer obj
                            X509 issuer = certObj2X509(issuerObj);

                            //check exist issuer obj in hash
                            X509 existen_issuer = result_map.get(new String(issuer.getDer()));
                            if (existen_issuer != null) {
                                cert.setIssuer(existen_issuer);
                            } else {
                                cert.setIssuer(issuer);
                                result_map.put(new String(issuer.getDer()), issuer);
                            }


                        }
                        break;
                    }
                }
            }
            if (!object_already_exist) {
                result_map.put(new String(cert.getDer()), cert);
            }
        }

        try {
            certDao.saveAll(result_map.values());
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }

        return null;
    }

    public Pkcs10ExtDTO getExtPkcs10(Long id) throws PKIServiceException {
        PKCS10 pkcs10_model;

        try {
            pkcs10_model = requestDao.findById(id);
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }

        Pkcs10ExtDTO extDTO = pkcs10_model.createExtDTO();

        List requestObjList = pkiEngine.readRequest(pkcs10_model.getDer());
        List<String> attributesList;

        try {
            attributesList = asn1Engine.printASN1(null, (byte[]) requestObjList.get(1));
        } catch (ASN1EngineException e) {
            throw new PKIServiceException(e);
        }

        if (attributesList != null) {
            for (String attr : attributesList) {
                logBuilder.append(attr).append(" \n");
            }
            logBuilder.writeInfo();
        }

        return extDTO;


    }

    public X509ExtDTO getExtX509(Long id) throws PKIServiceException {
        X509 x509_model;

        try {
            x509_model = certDao.findById(id);
        } catch (GenericDaoException e) {
            throw new PKIServiceException(e);
        }

        X509ExtDTO ext_dto = x509_model.createExtDTO();

        Set<X509Certificate> objs;
        try {
            objs = pkiEngine.readCert(x509_model.getDer());
        } catch (PKIEngineException e) {
            throw new PKIServiceException(e);
        }
        X509Certificate cert = objs.iterator().next(); //use first cert

        //public key
        PublicKey pkey = cert.getPublicKey();
        if (pkey instanceof DSAPublicKey) {
        } else if (pkey instanceof RSAPublicKey) {
            byte[] pkey_modulus_barray = ((RSAPublicKey) pkey).getModulus().toByteArray();
            ext_dto.setPubkey_modulus(Utils.byteArray2HexString(pkey_modulus_barray));
            ext_dto.setPubkey_modulus_length((pkey_modulus_barray.length - 1) * 8);
            BigInteger pkey_exponent = ((RSAPublicKey) pkey).getPublicExponent();
            ext_dto.setPubkey_exponent(String.format("%d (0x%x)", pkey_exponent, pkey_exponent));
        }

        byte[] pkey_barray = cert.getPublicKey().getEncoded();
        //ext_dto.setPubKey( byteArrayToOpensslString(pkey_barray) );
        ext_dto.setPubKey_length((pkey_barray.length - 1) * 8);

        //other attributes
        ext_dto.setDN(cert.getSubjectDN().toString());
        ext_dto.setVersion(cert.getVersion());
        ext_dto.setSignature_alg(cert.getSigAlgName());
        ext_dto.setIssuerDN(cert.getIssuerDN().toString());
        ext_dto.setPubKey_alg(pkey.getAlgorithm());


        ext_dto.setExtensions(new ArrayList<ExtensionDTO>());

        Set<String> extensions = cert.getCriticalExtensionOIDs();
        extensions.addAll(cert.getNonCriticalExtensionOIDs());

        for (String oid : extensions) {
            ExtensionDTO ext_dto_extension = new ExtensionDTO(oid);
            ext_dto_extension.setCritical(cert.getCriticalExtensionOIDs().contains(oid));

            byte value_encoded[] = cert.getExtensionValue(oid);
            if (value_encoded != null && value_encoded.length > 0) {

                String name = OidDecoder.lookup(oid);
                ext_dto_extension.setName(name == null ? oid : name);

                if (oid.equals(OidDecoder.basicConstraints)) {

                    ext_dto_extension.getValues().add("CA:" + ((cert.getBasicConstraints() == -1) ? "FALSE" : "TRUE"));

                } else if (oid.equals(OidDecoder.extendedKeyUsage)) {
                    try {
                        List<String> values = cert.getExtendedKeyUsage();
                        List<String> decodedValues = new ArrayList<String>();
                        for (String s : values) {
                            String printableValue = OidDecoder.lookup(s);
                            decodedValues.add(printableValue == null ? s : printableValue);
                        }
                        ext_dto_extension.getValues().addAll(decodedValues);
                    } catch (CertificateParsingException e) {
                        throw new PKIServiceException(e);
                    }

                } else if (oid.equals(OidDecoder.keyUsage)) {
                    List<String> values = OidDecoder.decodeKeyUsage(cert.getKeyUsage());
                    ext_dto_extension.getValues().addAll(values);

                } else {
                    if (cert.getExtensionValue(oid) != null) {
                        try {
                            List<String> strings = asn1Engine.printASN1(oid, cert.getExtensionValue(oid));
                            if (strings != null) {
                                ext_dto_extension.getValues().addAll(strings);
                            }
                        } catch (ASN1EngineException e) {
                            throw new PKIServiceException(e);
                        }
                    }
                }

            }
            ext_dto.getExtensions().add(ext_dto_extension);
        }

        return ext_dto;

    }

    public X509Certificate tmpSign() throws PKIServiceException {
        return null;
    }

}
